Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reactify visualize listing table #14227

Merged
merged 24 commits into from
Oct 12, 2017

Conversation

stacey-gammon
Copy link
Contributor

@stacey-gammon stacey-gammon commented Sep 29, 2017

Third times a try?

Let see if we can come to an agreement. :)

I have to add in another table similar to the dashboard, visualization and management tables, so decided to pick this back up again.

Tried to start with a very non-refactored, stateless version in the ui framework. Only migrated visualizations over, though dashboards would be next.

@cjcenizal - before I take this much further, would you mind, when you get a chance, to take a look at the listing table component and lmk what you think? I can also move it out of the uiframework and into ui/public/listingtable if you think it doesn't belong in the ui framework. Or we can discuss again how best to refactor/unrefactor.

@chrisronline - I heard you have been struggling with trying to find the right way to refactor these, or similar, table elements with sorting, paging, selecting. Lets get together some time next week and share notes! Would love to see this problem solved, one way or another.

Oh and P.S. -- this isn't a final version, just a start. There would still be a ton of duplicated code in vis & dash (and others) listing pages.

@cjcenizal
Copy link
Contributor

@stacey-gammon Nice! I like this approach so far. I think this is a great starting point because the abstraction hasn't really overcommitted in any particular direction. Everything that's abstracted looks reasonable, and the person using the ListingTable component has control over things like pagination and table actions.

I think from here we have some interesting options in terms of how to abstract further -- I'm down to talk about it if you want, or feel free to just ping me for more reviews.

@cjcenizal
Copy link
Contributor

cjcenizal commented Sep 29, 2017

I think it's OK to leave this in the UI Framework too, just please prefix the React components with "Kui", e.g. KuiListingTable.

@stacey-gammon
Copy link
Contributor Author

stacey-gammon commented Oct 3, 2017

@cjcenizal - what do you think about the doc site. Should I change ControlledTable examples to ListingTable examples? Or create duplicates? Visually, they look almost exactly the same, just the code differs.

I'm leaning towards switching it to ListingTable. A benefit of the ListingTable is that the checkbox selection logic works and the footer # of items selected updates naturally.

@cjcenizal
Copy link
Contributor

@stacey-gammon Sounds good to me!

@stacey-gammon stacey-gammon force-pushed the react/listing-tables branch 4 times, most recently from 0908d08 to fca387b Compare October 3, 2017 20:30
Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔔 This is really great, Stacey! This is nicely abstracted, it's pretty easy to understand, I feel like the consumer still has a lot of control over the table, and it clearly reduces the amount of code to create a listing page.

I left a lot of comments but the majority of them are nitpicky ones. I only left one or two which are significant but I think they're in line with the direction you've established here. Let me know what you think!

window.location = '#/visualize/new';
}

renderToolbarActions() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, could you do a find/replace and change all references of "Toolbar" to "ToolBar" and "toolbar" to "toolBar"?

RIGHT_ALIGNMENT
} from '../../../../services';

function wrapInRowCell(cell, key, alignment) {
Copy link
Contributor

@cjcenizal cjcenizal Oct 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but you could set a default value of alignment = LEFT_ALIGNMENT which would eliminate the need for the ternary expression.


KuiListingTable.PropTypes = {
columns: PropTypes.arrayOf(PropTypes.node),
rows: PropTypes.arrayOf(PropTypes.shape({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add defaultProps for both rows and selectedRowIds and set these to an empty array [] by default. This way the logic which refers to their length property won't throw an error if these aren't provided. I think this makes sense because from a consumer's standpoint it should be valid to not provide these values if there are no rows or if none are selected.

}

renderToolbarActions() {
return [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a key prop on each of these elements. I think this goes for the other examples as well.


renderToolbarActions() {
return [
<KuiButton
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key is needed for these buttons.


KuiListingTableToolbar.propTypes = {
onFilter: PropTypes.func.isRequired,
pagerComponent: PropTypes.node,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can drop the "Component" from names and just make this pager and actionComponent can become actions. Otherwise we're sort of implementing Hungarian notation.

<KuiToolBarFooter>
<KuiToolBarFooterSection>
<KuiToolBarText>
{itemsSelectedCount > 0 && `${itemsSelectedCount} items selected`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update this logic so that if itemsSelectedCount is 1 then the noun is singular ("item")?

}

KuiListingTableToolbarFooter.PropTypes = {
pagerComponent: PropTypes.node,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be pager, per an earlier comment.

import PropTypes from 'prop-types';

import { SortableProperties } from 'ui_framework/services';
import { Pager } from 'ui/pager';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably migrate this into the UI Framework. Probably not necessarily as part of this PR though.

columns={this.renderColumns()}
onFilter={this.fetchItems}
filter={this.state.filter}
noItemsPrompt={<NoVisualizationsPrompt />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we combine noItemsPrompt and loading into a single prompt prop, and offload the logic for determining when to show which into the consumer? If a prompt is specified, it will be presented instead of the table. This gives the consumer some more control over exactly what the prompt looks like and what its content is.

);
}

renderPrompt() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW our style recommends early exiting so this would become:

renderPrompt() {
  if (this.state.isFetchingItems) {
    return <KuiListingTableLoadingPrompt />;
  }

  if (this.items.length === 0) {
    if (this.state.filter) {
      return <KuiListingTableNoMatchesPrompt />;
    }
    
    return <NoVisualizationsPrompt />;
  }

  return null;
}

This makes it a little easier (for me at least) to mentally map conditions to outcome.

isChecked => isSelected

contents => content

refactor KuiTableRowCell internally
@stacey-gammon
Copy link
Contributor Author

I think this should be ready for another look @cjcenizal, with all changes addressed.

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 Awesome work! I found a few minor nitpicky things but after they're addressed I'm 👍 to merging.

You might want to pass our ideas about the header and rows props by @kjbekkelund to see if he can think of any improvements to make before we merge.

Thanks for letting me help out with this one!

prompt,
}) {

function areAllRowsChecked() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to areAllRowsSelected for consistency?

<KuiToolBarFooterSection>
<KuiToolBarText>
{itemsSelectedCount === 1 && '1 item selected'}
{itemsSelectedCount > 1 && `${itemsSelectedCount} items selected`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a little odd because these two conditions are implicitly mutually exclusive, but the second one will still be evaluated even if the first one is true. How about making it more explicit with a function?

export function KuiListingTableToolBarFooter({ pager, itemsSelectedCount }) {
  const renderText = () => {
    if (itemsSelectedCount === 1) {
      return "1 item selected";
    }

    return `${itemsSelectedCount} items selected`;
  };

  return (
    <KuiToolBarFooter>
      <KuiToolBarFooterSection>
        <KuiToolBarText>
          {renderText()}
        </KuiToolBarText>
      </KuiToolBarFooterSection>

      <KuiToolBarFooterSection>
        {pager}
      </KuiToolBarFooterSection>
    </KuiToolBarFooter>
  );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, getting too fancy! :) It's more readable your way, will update.

KuiListingTableLoadingPrompt
} from 'ui_framework/components';

export class VisualizeListingTable extends React.Component {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the pattern I've been seeing more lately is to import Component from the React module instead.

import React, {
  Component,
} from 'react';

export class VisualizeListingTable extends Component {

RIGHT_ALIGNMENT
} from '../../../../services';

export class ListingTable extends React.Component {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

];
}

renderHeader() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a method? Can we define this in the constructor instead?

@stacey-gammon
Copy link
Contributor Author

@kjbekkelund or @chrisronline - let me know if you guys want, or have time, to take a glance at this.

Copy link
Contributor

@chrisronline chrisronline left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this is looking great! The code is very readable and easy to understand! Love it! ++

I just had a couple high level questions about a couple things!

selectedRowIds={this.state.selectedRowIds}
rows={this.createRows()}
header={this.renderHeader()}
onFilter={this.fetchItems}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems strange to me. If this is indeed a filter, we shouldn't need to make network requests when it changes, right? We just need to look at the superset of data, and filter it down to some subset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately it does require a re-fetch because we don't grab all the items, we only grab the first 100. Eventually we'll implement paging, which will still require a re-fetch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing that's for performance issues? Have we explored doing a scroll with the ES search so we can continue searching in the background while results are available? It might not be a big deal, but it seems like we could show the initial 100, then quickly load the rest behind the scene?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of our top notch community members explored this but ran into some complications with sorting and our index not having keyword fields for type and description. Read through #11415 to get the background. Something we want to do, but haven't had time to dig in deeper. We closed the issue because as a workaround we have a configurable setting to how many items can be retrieved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Makes sense!

}

KuiListingTable.PropTypes = {
header: PropTypes.arrayOf(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the thinking behind this being data drive, instead of component driven? Like why not construct some component in the caller and pass it in directly?

This is what is passed in now:

return [
      {
        content: 'Title',
        onSort: this.sortByTitle,
        isSorted: this.state.sortedColumn === 'title',
        isSortAscending: this.sortableProperties.isAscendingByName('title'),
      },
      {
        content: 'Type',
        onSort: this.sortByType,
        isSorted: this.state.sortedColumn === 'type',
        isSortAscending: this.sortableProperties.isAscendingByName('type'),
      },
    ];

But what if we just did:

return [
  <KuiTableHeaderCell
       onSort: this.sortByTitle,
        isSorted: this.state.sortedColumn === 'title',
        isSortAscending: this.sortableProperties.isAscendingByName('title')
   >
      Title
   </KuiTableHeaderCell>
];

or something similar?

It feels we have too much unnecessary code around this header concept and a simpler solution might work out better. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This same question/concept applies to maybe some of the other props here, like rows.

I think I understand the inclination to separate the data from the presentational concerns/details, but it feels the coupling is pretty strong already so I'm not sure it's buying us much.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was that way at first. It changed in this commit- f50f916
after a discussion with @cjcenizal. I like that it's more refactored in the data driven approach. Comment that initiated the change is here: f50f916#r142833708

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that makes sense. I'm not sure I love it still, but it's perfectly readable and still easy to understand so carry on!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, I've copy/pasted my original comment below. I was hoping that by encapsulating the row, header, and cell components within the table component, it would let the consumer use a simpler interface to specify their content. For example, if the rows' cells just needs to contain text, you can specify each row as an array of strings instead of needing to specify each cell component too. I was also hoping that that this would move some of the sorting logic into KuiListingTable.

That said, I'm not 100% sold on this approach and really welcome people's thoughts on it. I'd love it if we could improve these abstractions, but I'm also OK with reverting it if people find them too difficult to use.


I wonder if there's a way where we can create abstractions for the way headers and rows present data. If the interfaces of these abstractions were to be similar then I think that would be ergonomic for the consumer, since they're very similar components (they're both containers of the same number of cells).

I'm thinking of something like this:

// This component is responsible for instantiating `KuiTableRowCell` instances.
// It will iterate over each element in the cells array. You can specify a cell as a string,
// an element, a number, or a more complex configuration object. Depending
// on how the type is formatted, the values will either be passed as `children`
// or as various props to each `KuiTableRowCell`. 

const rows = this.items.map(item => (
  <KuiListingTableRow
    cells={[
      <a className="kuiLink" href={item.link}>item.name</a>,
      <div className={`kuiIcon kuiIcon--success ${item.icon}`}/>,
      item.date,
      {
        content: item.magnitude,
        align: 'right',
      },
    ]}
  />
));

// Same idea here.

const header = <KuiListingTableHeader
  cells={[{
    {
      content: 'Name',
      sortableId: 'name, // The presence of this property will present a column as sortable
    },
    'Icon',
    {
      content: 'Date',
      sortableId: 'date,
    }, {
      content: 'Magnitude',
      align: 'right',
      sortableId: 'magnitude',
    }
  }]}
/>

// And then maybe we could leverage `children' and the table would become this:

<KuiListingTable
  pager={this.renderPager()}
  toolbarActions={this.renderToolbarActions()}
  selectedRowIds={this.state.selectedRowIds}
  header={header}
  onFilter={() => {}}
  filter=""
  onItemSelectionChanged={this.onItemSelectionChanged}
>
  {rows}
</KuiListingTable>

What do you think? Does this sound crazy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, as @cjcenizal said, you can still pass an array of nodes or strings for the header and rows, it's just the internal part, not to include the KuiTableRowCell part. But this means we have to pass in sort information and align information sometimes, for the props that go on that outer element.

I'm also flexible if we find a better approach. For now I'll go ahead and submit once this passes, and we can update when/if we find some better abstractions or patterns. Perhaps once we add in an HOC and refactor the sorting logic, we'll find a better way to handle it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's easy to follow and totally makes sense. I can't articulate my thoughts against it that well, which could easily indicate they aren't grounded in anything useful. Please continue as is and I'm sure we'll find another time to rethink it and maybe have some more useful conversations (on my side at least!)

Copy link
Contributor

@chrisronline chrisronline left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@kimjoar kimjoar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. Should we get some tests in before merging? A fair bit of "complexity" here.

@@ -93,62 +93,62 @@ export default props => (
</GuideSection>

<GuideSection
title="ControlledTable"
title="ListingTable"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it also show one with sorting? There's also some mention of innerTable, should we show that here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The innerTable isn't it's own component. We could move it out, but for now, it shouldn't get it's own section in the docs view. I will give one of the headers sort attributes.

@stacey-gammon stacey-gammon merged commit c3222ad into elastic:master Oct 12, 2017
stacey-gammon added a commit to stacey-gammon/kibana that referenced this pull request Oct 12, 2017
* Reactify visualize listing table

* refactor toolbar => toolBar

* use prompt prop to also cover loading and didn't match search logic.

* Use default alignment instead of ternary

* Add keys to array elements where missing

* Add align property to KuiTableHeaderCell

* Fix issue with filter not showing up in search tool bar

* onCheckChanged => onSelectionChanged

* pagerComponent => pager, actionComponent => actions

* use singular verbiage when only 1 item is selected

* exit early per style guide

* fix lint errors

* rename columns => header

* Refactor KuiTableHeaderCell into KuiListingTable

isChecked => isSelected

contents => content

refactor KuiTableRowCell internally

* fix lint errors

* areAllRowsChecked => areAllRowsSelected

* improve itemSelectedCount logic in KuiListingTableToolBarFooter

* React.Component => Component

* make header data a variable, not a function

* Only consider all rows selected if rows exist and they are all selected, not if there are no rows.

* Adding a few KuiListingTable tests

* Give one column sort attributes in examples page
stacey-gammon added a commit that referenced this pull request Oct 12, 2017
* Reactify visualize listing table

* refactor toolbar => toolBar

* use prompt prop to also cover loading and didn't match search logic.

* Use default alignment instead of ternary

* Add keys to array elements where missing

* Add align property to KuiTableHeaderCell

* Fix issue with filter not showing up in search tool bar

* onCheckChanged => onSelectionChanged

* pagerComponent => pager, actionComponent => actions

* use singular verbiage when only 1 item is selected

* exit early per style guide

* fix lint errors

* rename columns => header

* Refactor KuiTableHeaderCell into KuiListingTable

isChecked => isSelected

contents => content

refactor KuiTableRowCell internally

* fix lint errors

* areAllRowsChecked => areAllRowsSelected

* improve itemSelectedCount logic in KuiListingTableToolBarFooter

* React.Component => Component

* make header data a variable, not a function

* Only consider all rows selected if rows exist and they are all selected, not if there are no rows.

* Adding a few KuiListingTable tests

* Give one column sort attributes in examples page
@stacey-gammon stacey-gammon deleted the react/listing-tables branch October 24, 2017 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants